export interface IEventHandler {
    (data?: any): any;
}

interface IEventMap {
    [type: string]: IEventHandler[];
}

export interface IEvent {
    type: string;
    sender: unknown;
    handled?: boolean;
    data?: unknown;
    [name: string]: unknown;
}

export abstract class Eventable {
    #events: IEventMap;

    protected constructor() {
        this.#events = {};
    }

    on(type: string, handler: IEventHandler): this {
        type = type.toLowerCase();
        (this.#events[type] ??= []).push(handler);
        return this;
    }

    off(type: string): this {
        delete this.#events[type.toLowerCase()];
        return this;
    }

    trigger(event: IEvent): this;
    trigger(type: string, data?: unknown, isExtend?: boolean): this;
    trigger(e: IEvent | string, data?: unknown, isExtend: boolean = true): this {
        const event: IEvent = typeof e === "string"
            ? {
                type: e.trim().toLowerCase(),
                sender: this
            }
            : e;

        const handlerList = this.#events[event.type];
        if (!handlerList) { return this; }

        if (isExtend && data && typeof data === "object") {
            Object.keys(data).forEach(key => {
                if (!/^(?:type|sender)$/.test(key)) {
                    event[key] = data[key];
                }
            });
        } else if ((data ?? null) !== null) {
            event.data = data;
        }

        for (const handler of handlerList) {
            handler(event);
            if (event.handled) {
                break;
            }
        }
        return this;
    }
}
